/*
 * JSF Toolkit Component Framework
 * Copyright (C) 2007 Noah Sloan <iamnoah A-T gmail D0T com>
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 */
package com.jsftoolkit.base.components;

import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.component.ContextCallback;
import javax.faces.component.UIColumn;
import javax.faces.component.UIComponent;
import javax.faces.component.UIData;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.FacesEvent;
import javax.faces.event.FacesListener;
import javax.faces.event.PhaseId;
import javax.faces.model.DataModel;

import com.jsftoolkit.base.ComponentHelp;
import com.jsftoolkit.base.JsfIterator;
import com.jsftoolkit.base.ResourceInfo;
import com.jsftoolkit.base.ComponentHelp.InputState;
import com.jsftoolkit.utils.Utils;

/**
 * General purpose data iterator. Unlike {@link UIData}, {@link DataIterator}
 * is not coupled to a particular rendering model. i.e. all child components
 * will be processed, not just {@link UIColumn}s. Other than this deviation,
 * its behavior should be identical to {@link UIData}s, except when using any
 * of the extensions mentioned below.
 * <p>
 * The "rowId" property allows you to specify the client identity of each data
 * item. As long as this value is unique, any actions performed on a particular
 * row will always be applied to the proper data item, regardless of changes to
 * the underlying {@link DataModel}.
 * <p>
 * In the event that an element with matching id is no longer in the DataModel,
 * the action will simply not occur.
 * <p>
 * Any facets added to this component will be ignored.
 * 
 * @see #getClientId(FacesContext)
 * @author noah
 * 
 */
public class DataIterator extends DataIteratorBase implements JsfIterator {

	private static final String STATE_MAP = "stateMap";

	private static final String ID_FORMAT = "%s" + SEPARATOR_CHAR + "%s";

	private static final java.util.logging.Logger LOG = java.util.logging.Logger
			.getLogger(DataIterator.class.getCanonicalName());

	/**
	 * If the attribute "rowId" is set, it will be evaluated for the current row
	 * to determine the clientId prefix for the current row's children.
	 * <p>
	 * If it is not set, the rowIndex is used.
	 */
	@Override
	public String getClientId(FacesContext context) {
		Utils.notNull(context, "context");

		ValueExpression rowId = getRowExpression();
		if (rowId == null || getRowIndex() < 0) {
			return super.getClientId(context);
		} else {
			String thisId = getContainerId(context);
			return String.format(ID_FORMAT, thisId, rowId.getValue(context
					.getELContext()));
		}
	}

	/**
	 * 
	 * @param context
	 * @return the clientId of this UIData component
	 */
	private String getContainerId(FacesContext context) {
		final int originalIndex = super.getRowIndex();

		// turn off iteration while we retrieve the id
		super.setRowIndex(-1);
		String thisId = super.getClientId(context);
		// restore the row index
		super.setRowIndex(originalIndex);
		return thisId;
	}

	/**
	 * Adds logic to resolve custom row ids if a custom row id expression was
	 * specified.
	 */
	@Override
	public boolean invokeOnComponent(FacesContext context, String clientId,
			ContextCallback callback) throws FacesException {
		Utils.notNull(context, "context");
		Utils.notNull(clientId, "clientId");

		ValueExpression rowExpression = getRowExpression();
		String baseId = getContainerId(context);

		// parse the rowId
		String rowId = getRowId(clientId, baseId);
		if (rowId == null) {
			// the component being looked for is not down this branch, so abort
			return false;
		}
		int originalIndex = getRowIndex();
		boolean found = false;
		try {
			if (rowExpression == null) {
				// XXX why doesn't super.invokeOnComponent work right?
				try {
					setRowIndex(Integer.parseInt(rowId));
					found = invokeOnChildren(context, clientId, callback);
				} catch (NumberFormatException e) {
					LOG.warning("Could not parse rowId: " + rowId);
					return false;
				}
			} else {
				setRowId(rowId, context);
				found = invokeOnChildren(context, clientId, callback);
			}
		} catch (Exception e) {
			throw new FacesException(e);
		} finally {
			setRowIndex(originalIndex);
		}

		return found;
	}

	public Set<ResourceInfo> getResources(FacesContext context,
			UIComponent component) {
		final LinkedHashSet<ResourceInfo> resources = new LinkedHashSet<ResourceInfo>();
		UIDataProcessor.iterate(context, (JsfIterator) this,
				new UIDataProcessor() {
					@Override
					public void processChild(FacesContext context,
							UIComponent child) {
						HtmlScripts.processChild(context, child, resources);
					}
				});
		return resources;
	}

	/**
	 * {@link UIData} will only save the descendant state for {@link UIColumn}s.
	 * %#@^&$#^&!!! So, we have to fix that too.
	 */
	@Override
	@SuppressWarnings("unchecked")
	public void setRowIndex(int rowIndex) {
		FacesContext context = getFacesContext();

		Map<String, InputState> stateMap = (Map<String, InputState>) getAttributes()
				.get(STATE_MAP);
		if (stateMap == null) {
			getAttributes().put(STATE_MAP,
					stateMap = new HashMap<String, InputState>());
		}

		// save the state of nested inputs
		for (UIComponent kid : getChildren()) {
			ComponentHelp.saveSubTreeInputsState(stateMap, kid, context);
		}

		// only worry about saving the descendant state, we'll let UIData handle
		// the DataModel
		super.setRowIndex(rowIndex);

		// restore the state from the last time we were on this row
		for (UIComponent kid : getChildren()) {
			ComponentHelp.loadSubTreeInputsState(stateMap, kid, context);
		}
	}

	/**
	 * Sets the rowIndex to the index of the row with the same rowId.
	 * 
	 * @param rowId
	 * @param context
	 *            the context to evaluate the row IDs in
	 */
	public void setRowId(String rowId, FacesContext context) {
		ValueExpression rowExpression = getRowExpression();
		Utils.notNull(rowExpression, "rowExpression");
		for (int i = 0; isSetRowAvailable(i); i++) {
			if (rowId.equals(Utils.toString(rowExpression.getValue(context
					.getELContext())))) {
				// the rowId is suppose to be unique, so if we found a
				// match, we're done
				break;
			}
		}

	}

	protected ValueExpression getRowExpression() {
		return getValueExpression(ROW_ID);
	}

	/**
	 * Calls {@link #invokeOnComponent(FacesContext, String, ContextCallback)}
	 * on each child node, reseting its id before the call.
	 * 
	 * @param context
	 * @param clientId
	 * @param callback
	 * @return
	 */
	protected boolean invokeOnChildren(FacesContext context, String clientId,
			ContextCallback callback) {
		for (UIComponent child : getChildren()) {
			// reset the child's id
			child.setId(child.getId());
			if (child.invokeOnComponent(context, clientId, callback)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Sets the row index and returns {@link #isRowAvailable()}.
	 * 
	 * @param index
	 * @return
	 */
	protected boolean isSetRowAvailable(int index) {
		setRowIndex(index);
		return isRowAvailable();
	}

	/**
	 * Given the child client id and the id of this component, returns the
	 * portion identifying the row.
	 * 
	 * @param clientId
	 * @param thisId
	 * @return the rowId, parsed from the client id.
	 */
	public static String getRowId(String clientId, String thisId) {
		if (!clientId.startsWith(thisId + SEPARATOR_CHAR)) {
			return null;
		}

		int startIndex = thisId.length() + 1;
		// find the next separator so we can parse the rowId
		int endIndex = clientId.indexOf(SEPARATOR_CHAR, startIndex);
		endIndex = endIndex == -1 ? clientId.length() : endIndex;
		if (startIndex <= endIndex && startIndex > 0
				&& endIndex <= clientId.length()) {
			return clientId.substring(startIndex, endIndex);
		}
		return null;
	}

	/**
	 * Calls {@link UIComponent#processDecodes(FacesContext)} on each child,
	 * once per row.
	 */
	@Override
	public void processDecodes(FacesContext context) {
		Utils.notNull(context, "context");
		if (!isRendered()) {
			return;
		}

		// clear the cached model in case the bound value has changed
		setDataModel(null);
		// let each child decode
		UIDataProcessor.iterate(context, (JsfIterator) this,
				new UIDataProcessor() {
					public void processChild(FacesContext context,
							UIComponent child) {
						if (child.isRendered()) {
							child.processDecodes(context);
						}
					}
				});

		// let our renderer (if any) decode
		decode(context);
	}

	/**
	 * Calls {@link UIComponent#processUpdates(FacesContext)} on each child,
	 * once per row.
	 */
	@Override
	public void processUpdates(FacesContext context) {
		Utils.notNull(context, "context");
		if (!isRendered()) {
			return;
		}

		UIDataProcessor.iterate(context, (JsfIterator) this,
				new UIDataProcessor() {
					public void processChild(FacesContext context,
							UIComponent child) {
						child.processUpdates(context);
					}
				});
	}

	/**
	 * Calls {@link UIComponent#processValidators(FacesContext)} on each child,
	 * once per row.
	 */
	@Override
	public void processValidators(FacesContext context) {
		Utils.notNull(context, "context");
		if (!isRendered()) {
			return;
		}

		UIDataProcessor.iterate(context, (JsfIterator) this,
				new UIDataProcessor() {
					public void processChild(FacesContext context,
							UIComponent child) {
						child.processValidators(context);
					}
				});
	}

	/**
	 * If a rowId expression was specified, then we need to wrap any events
	 * coming from our children so that we can properly set the row data when
	 * the event occurs (see {@link #broadcast(FacesEvent)}).
	 */
	@Override
	public void queueEvent(final FacesEvent event) {
		Utils.notNull(event, "event");
		Utils.notNull(getParent(), "parent");

		ValueExpression rowExpression = getRowExpression();
		if (rowExpression == null) {
			// if there is no row id expression, behave normally
			super.queueEvent(event);
		} else {
			// can't call super.queueEvent because the RI will put that stupid
			// package scoped wrapper class around the event (which means we
			// can't get to our event in broadcast or remove the wrapper)
			getParent().queueEvent(
					new WrappedEvent(this, rowExpression.getValue(FacesContext
							.getCurrentInstance().getELContext()), event));
		}

	}

	/**
	 * Checks to see if the event is a {@link WrappedEvent}, and if it is, sets
	 * the current row to match the rowId.
	 */
	@Override
	public void broadcast(FacesEvent event) throws AbortProcessingException {
		// it is possible that the cached DataModel has been replaced in the
		// backing bean (e.g. DropTarget does this), so we need to clear the
		// cached model for this event
		setDataModel(null);

		if (event instanceof WrappedEvent) {
			WrappedEvent we = (WrappedEvent) event;
			int oldRow = getRowIndex();
			setRowId(Utils.toString(we.rowId), FacesContext
					.getCurrentInstance());
			we.event.getComponent().broadcast(we.event);
			setRowIndex(oldRow);
		} else {
			super.broadcast(event);
		}
	}

	/**
	 * Wraps {@link FacesEvent}s so we can add rowId information. See
	 * {@link DataIterator#broadcast(FacesEvent)} and
	 * {@link DataIterator#queueEvent(FacesEvent)}.
	 * 
	 * @author noah
	 * 
	 */
	protected static class WrappedEvent extends FacesEvent {

		private static final long serialVersionUID = 1L;

		Object rowId;

		FacesEvent event;

		public WrappedEvent(UIComponent component, Object rowId,
				FacesEvent event) {
			super(component);
			this.rowId = rowId;
			this.event = event;
		}

		@Override
		public PhaseId getPhaseId() {
			return event.getPhaseId();
		}

		@Override
		public void setPhaseId(PhaseId phaseId) {
			event.setPhaseId(phaseId);
		}

		@Override
		public boolean isAppropriateListener(FacesListener listener) {
			return false;
		}

		@Override
		public void processListener(FacesListener listener) {
			// will never happen
			throw new IllegalStateException();
		}

	}
}
